package com.tsing.tsingfengmall.file.service.impl;

import com.github.benmanes.caffeine.cache.Cache;
import com.google.common.collect.HashMultimap;
import com.tsing.tsingfengmall.file.config.AsyncMinioClient;
import com.tsing.tsingfengmall.file.dto.TaskDTO;
import com.tsing.tsingfengmall.file.dto.TaskInfoDTO;
import com.tsing.tsingfengmall.file.exception.BadRequestException;
import com.tsing.tsingfengmall.file.service.IFileService;
import io.minio.*;
import io.minio.http.Method;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import io.minio.messages.Part;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.util.UriUtils;

import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.time.ZonedDateTime;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * @author TheTsing
 */
@Service
public class FileServiceImpl implements IFileService {

    @Value("${minio.bucket}")
    private String bucket;

    private final MinioClient minioClient;

    private final AsyncMinioClient asyncMinioClient;

    private final Cache<String, TaskInfoDTO> taskCache;

    public FileServiceImpl(MinioClient minioClient, AsyncMinioClient asyncMinioClient, @Qualifier("taskCache") Cache<String, TaskInfoDTO> taskCache) {
        this.minioClient = minioClient;
        this.asyncMinioClient = asyncMinioClient;
        this.taskCache = taskCache;
    }

    private void existBucket() {
        try {
            if (!minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucket).build())) {
                minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucket).build());
            }
        } catch (Exception e) {
            throw new BadRequestException("无法访问存储桶");
        }
    }

    private boolean existObject(String objectName) {
        try {
            minioClient.statObject(StatObjectArgs.builder().bucket(bucket).object(objectName).build());
        } catch (Exception e) {
            return false;
        }
        return true;
    }

    private String generateObjectName(String objectName) {
        if (!existObject(objectName)) {
            return objectName;
        }
        int endIndex = objectName.lastIndexOf(46);
        StringBuilder stringBuilder = new StringBuilder(objectName);
        if (endIndex == -1) {
            stringBuilder.append("(1)");
        } else stringBuilder.insert(endIndex, "(1)");
        return generateObjectName(stringBuilder.toString());
    }

    @Override
    public String upload(MultipartFile file) {
        existBucket();
        String filePath = new SimpleDateFormat("yyyy/MM/dd").format(new Date());
        String objectName = generateObjectName(filePath + "/" + file.getOriginalFilename());
        try (InputStream inputStream = file.getInputStream()) {
            minioClient.putObject(PutObjectArgs.builder()
                    .bucket(bucket)
                    .object(objectName)
                    .stream(inputStream, inputStream.available(), -1)
                    .contentType(file.getContentType())
                    .build());
        } catch (Exception e) {
            throw new BadRequestException("文件上传失败");
        }
        return objectName;
    }

    @Override
    public List<String> uploads(MultipartFile[] files) {
        existBucket();
        List<String> objectNames = new ArrayList<>(files.length);
        String filePath = new SimpleDateFormat("yyyy/MM/dd").format(new Date());
        for (MultipartFile file : files) {
            String objectName = generateObjectName(filePath + "/" + file.getOriginalFilename());
            try (InputStream inputStream = file.getInputStream()) {
                minioClient.putObject(PutObjectArgs.builder()
                        .bucket(bucket)
                        .object(objectName)
                        .stream(inputStream, inputStream.available(), -1)
                        .contentType(file.getContentType())
                        .build());
            } catch (Exception e) {
                throw new BadRequestException("文件上传失败");
            }
            objectNames.add(objectName);
        }
        return objectNames;
    }

    @Override
    public Map<String, String> policy(String name, Integer maxFileSize, Integer expire) {
        existBucket();
        String filePath = new SimpleDateFormat("yyyy/MM/dd").format(new Date());
        String objectName = generateObjectName(filePath + "/" + name);
        Map<String, String> formData = new HashMap<>(16);
        formData.put("key", objectName);
        PostPolicy postPolicy = new PostPolicy(bucket, ZonedDateTime.now().plusMinutes(expire));
        postPolicy.addContentLengthRangeCondition(0, maxFileSize);
        postPolicy.addEqualsCondition("key", objectName);
        try {
            formData.putAll(minioClient.getPresignedPostFormData(postPolicy));
            return formData;
        } catch (Exception e) {
            throw new BadRequestException("文件上传失败");
        }
    }

    @Override
    public String access(String url, Boolean download, Integer expire) {
        if (!existObject(url)) {
            throw new BadRequestException("文件不存在");
        }
        GetPresignedObjectUrlArgs.Builder builder = GetPresignedObjectUrlArgs.builder()
                .method(Method.GET)
                .bucket(bucket)
                .object(url)
                .expiry(expire, TimeUnit.MINUTES);
        if (download) {
            Map<String, String> headers = new HashMap<>(4);
            headers.put("response-content-type", MediaType.APPLICATION_OCTET_STREAM_VALUE);
            builder.extraQueryParams(headers);
        }
        GetPresignedObjectUrlArgs args = builder.build();
        try {
            String completedUri = minioClient.getPresignedObjectUrl(args);
            int index = completedUri.indexOf(bucket) + bucket.length();
            return completedUri.substring(index);
        } catch (Exception e) {
            throw new BadRequestException("获取文件失败");
        }
    }

    @Override
    public ResponseEntity<Resource> download(String url) {
        if (!existObject(url)) {
            throw new BadRequestException("文件不存在");
        }
        try {
            InputStream inputStream = minioClient.getObject(GetObjectArgs.builder().bucket(bucket).object(url).build());
            ContentDisposition disposition = ContentDisposition.attachment().filename(UriUtils.encode(url.substring(url.lastIndexOf(47) + 1), StandardCharsets.UTF_8)).build();
            return ResponseEntity.ok()
                    .header(HttpHeaders.CONTENT_DISPOSITION, disposition.toString())
                    .contentType(MediaType.APPLICATION_OCTET_STREAM)
                    .body(new InputStreamResource(inputStream));
        } catch (Exception e) {
            throw new BadRequestException("获取文件失败");
        }
    }

    @Override
    public void delete(String url) {
        deletes(Collections.singletonList(url));
    }

    @Override
    public void deletes(List<String> urls) {
        List<DeleteObject> deleteObjects = urls.stream().map(DeleteObject::new).collect(Collectors.toList());
        Iterable<Result<DeleteError>> results = minioClient.removeObjects(
                RemoveObjectsArgs.builder().bucket(bucket).objects(deleteObjects).build());
        try {
            for (Result<DeleteError> result : results) {
                result.get();
            }
        } catch (Exception e) {
            throw new BadRequestException("删除文件失败");
        }
    }

    @Override
    public Map<String, Object> partInit(String name, Long size, Integer partSize) {
        existBucket();
        String filePath = new SimpleDateFormat("yyyy/MM/dd").format(new Date());
        String objectName = generateObjectName(filePath + "/" + name);
        HashMultimap<String, String> headers = HashMultimap.create();
        headers.put(HttpHeaders.CONTENT_TYPE, MediaTypeFactory.getMediaType(objectName).orElse(MediaType.APPLICATION_OCTET_STREAM).toString());
        String uploadId;
        try {
            CreateMultipartUploadResponse multipartUploadResponse = asyncMinioClient.createMultipartUploadAsync(bucket, null, objectName, headers, null).get();
            uploadId = multipartUploadResponse.result().uploadId();
        } catch (Exception e) {
            throw new BadRequestException("创建分片上传任务失败");
        }
        List<TaskDTO> uploadUrls = new ArrayList<>();
        int partCount = (int) Math.ceil((double) size / partSize);
        Map<String, String> reqParams = new HashMap<>(4);
        reqParams.put("uploadId", uploadId);
        try {
            for (int i = 1; i <= partCount; i++) {
                reqParams.put("partNumber", String.valueOf(i));
                String preSignUploadUrl = minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
                        .method(Method.PUT)
                        .bucket(bucket)
                        .object(objectName)
                        .expiry(2, TimeUnit.HOURS)
                        .extraQueryParams(reqParams)
                        .build());
                uploadUrls.add(new TaskDTO(i, preSignUploadUrl.substring(preSignUploadUrl.indexOf(bucket) + bucket.length())));
            }
        } catch (Exception e) {
            throw new BadRequestException("创建分片上传任务失败");
        }
        Map<String, Object> resultMap = new HashMap<>(16);
        resultMap.put("uploadId", uploadId);
        resultMap.put("partCount", partCount);
        resultMap.put("uploadUrls", uploadUrls);
        taskCache.put(uploadId, new TaskInfoDTO(objectName, partCount));
        return resultMap;
    }

    @Override
    public String partMerge(String uploadId) {
        TaskInfoDTO taskInfoDTO = taskCache.getIfPresent(uploadId);
        if (Objects.isNull(taskInfoDTO)) {
            throw new BadRequestException("未找到该任务");
        }
        ListPartsResponse listPartsResponse;
        try {
            listPartsResponse = asyncMinioClient.listPartsAsync(bucket, null, taskInfoDTO.getObjectName(), taskInfoDTO.getPartCount(), 0, uploadId, null, null).get();
        } catch (Exception e) {
            throw new BadRequestException("合并分片失败");
        }
        List<Part> parts = listPartsResponse.result().partList();
        if (parts.size() != taskInfoDTO.getPartCount()) {
            throw new BadRequestException("分片缺失，请重新上传");
        }
        try {
            asyncMinioClient.completeMultipartUploadAsync(bucket, null, taskInfoDTO.getObjectName(), uploadId, parts.toArray(new Part[0]), null, null).get();
        } catch (Exception e) {
            throw new BadRequestException("合并分片失败");
        }
        taskCache.invalidate(uploadId);
        return taskInfoDTO.getObjectName();
    }

    @Override
    public void partAbort(String uploadId) {
        TaskInfoDTO taskInfoDTO = taskCache.getIfPresent(uploadId);
        if (Objects.isNull(taskInfoDTO)) {
            throw new BadRequestException("未找到该任务");
        }
        try {
            asyncMinioClient.abortMultipartUploadAsync(bucket, null, taskInfoDTO.getObjectName(), uploadId, null, null).get();
        } catch (Exception e) {
            throw new BadRequestException("终止分片上传失败");
        }
        taskCache.invalidate(uploadId);
    }

    @Override
    public List<Integer> partUploaded(String uploadId) {
        TaskInfoDTO taskInfoDTO = taskCache.getIfPresent(uploadId);
        if (Objects.isNull(taskInfoDTO)) {
            throw new BadRequestException("未找到该任务");
        }
        if (existObject(taskInfoDTO.getObjectName())) {
            throw new BadRequestException("文件已经上传完成");
        }
        ListPartsResponse listPartsResponse;
        try {
            listPartsResponse = asyncMinioClient.listPartsAsync(bucket, null, taskInfoDTO.getObjectName(), taskInfoDTO.getPartCount(), 0, uploadId, null, null).get();
        } catch (Exception e) {
            throw new BadRequestException("获取分片列表失败");
        }
        return listPartsResponse.result().partList().stream().map(part -> part.partNumber()).collect(Collectors.toList());
    }

}
